{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Marketing Campaign Optimization"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Objective and Prerequisites\n",
    "\n",
    "Companies across almost every industry are looking to optimize their marketing campaigns. In this Jupyter Notebook, we’ll explore a marketing campaign optimization problem that is common in the banking and financial services industry, which involves determining which products to offer to individual customers in order to maximize total expected profit while satisfying various business constraints. You’ll learn how to formulate a mathematical optimization model of the problem (using machine learning predictive response models as parameters) and solve it using the Gurobi Optimizer.\n",
    "\n",
    "This modeling example is at the beginner level, where we assume that you know Python and that you have some knowledge about building mathematical optimization models. The reader should also consult the  [documentation](https://www.gurobi.com/resources/?category-filter=documentation)\n",
    "of the Gurobi Python API.\n",
    "\n",
    "**Download the Repository** <br />\n",
    "You can download the repository containing this and other examples by clicking [here](https://github.com/Gurobi/modeling-examples/archive/master.zip). "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Motivation\n",
    "\n",
    "The main goal of marketing in the banking and financial services industry is to offer \"the right product to the right customer at the right time\". However, \n",
    "actually being able to achieve this goal is a complicated and challenging undertaking. What makes this particularly difficult is that companies have multiple products and operate under a complex set of business constraints. Choosing which products to offer to which customers in order to maximize the marketing return on investment and satisfy the business constraints is enormously complex.\n",
    "\n",
    "Consider a major bank  that has made a deliberate effort to become a customer-focused institution, as opposed to a vertical product driven company. The goal of the bank is \"to be the best at helping customers become financially better off by providing relevant solutions to their unique needs\". A direct consequence of this goal is that marketing campaigns are multiple product campaigns as opposed to single product campaigns. \n",
    "This transforms the data science and campaign targeting process from a fairly simple application of individual response models into a significantly more complex process that invloves  choosing which product to offer to which customer and through which channel.\n",
    "\n",
    "The marketing team of the bank are used to applying business rules to target customers directly. \n",
    "For example, \n",
    "they target \n",
    "customers solely on their product gaps or on marketers' business intuition. \n",
    "The bank's\n",
    "marketers have also applied RFM type analysis where general recency, frequency, and monetary measurements as well as product gaps are used to target customers for specific offers. \n",
    "\n",
    "The marketing team's current approach, which \n",
    "is widely used, \n",
    "relies on predictive response models to target customers for offers. These models estimate the probability that a customer will respond to a specific offer and can significantly increase the response rate to a product offering. However, simply knowing a customer's probability of responding to a particular offer is not enough when a company has several products to promote and other business constraints to consider in its marketing planning.\n",
    "\n",
    "Generally speaking, marketing teams also face the problem of knowing which product to offer to a customer, not just which customer to offer a product. In practice, many ad hoc rules are used:\n",
    "\n",
    "* prioritization rules based on response rates or estimated expected profitability measures\n",
    "\n",
    "* business rules to prioritize products that can be marketed\n",
    "\n",
    "* product response models to select customers for a particular campaign. \n",
    "\n",
    "One approach that is easily implemented but may not produce optimal customer contact plans relies on a measure of expected offer profitability to choose which products to offer customers. However, a shortcoming of this approach is its inability to effectively handle complex constraints on the customer contact plan.\n",
    "\n",
    "To address this marketing campaign optimization problem, M. D. Cohen [1] proposed a MIP approach with data from Scotiabank. The marketing campaign optimization problem considered eleven unique offers: five investment, three lending, and three day-to-day banking offers. The investment offers included Guaranteed Investment Certificates (GICs), mutual funds, Registered Education Savings Program (RESP) and two unique discount brokerage offers. The lending offers included a mortgage and two credit card offers. \n",
    "The day-to-day banking offers included one of two Scotia online banking service offers and a deposit account acquisition. The term campaign is used here to imply one large pro-active customer contact campaign that is comprised of eleven distinct offers; it can be thought of as eleven single product campaigns that are being offered at generally the same time to a non-overlapping set of customers. Approximately 2.5 million customers were included in the potential target market for the campaign.\n",
    "\n",
    "\n",
    "In this Jupyter Notebook, we will use this MIP approach to address the bank’s marketing campaign optimization problem. It should be noted that this approach could be used by virtually any company across various industries to optimize their marketing campaigns, while taking into account their business constraints. \n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Problem Description\n",
    "The bank's marketing team needs to determine what products to offer to each customer in a way that maximizes the marketing campaign return on investment while considering the following constraints: \n",
    "\n",
    "* limits on funding available for the campaign.\n",
    "* restrictions on the minimum number of product offers that can be made in a campaign.\n",
    "* campaign return-on-investment hurdle rates that must be met."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Solution Approach\n",
    "\n",
    "Mathematical programming is a declarative approach where the modeler formulates a mathematical optimization model that captures the key aspects of a complex decision problem. The Gurobi Optimizer solves such models using state-of-the-art mathematics and computer science.\n",
    "\n",
    "A mathematical optimization model has five components, namely:\n",
    "\n",
    "* Sets and indices.\n",
    "* Parameters.\n",
    "* Decision variables.\n",
    "* Objective function(s).\n",
    "* Constraints.\n",
    "\n",
    "We now present a MIP approach for this marketing campaign optimization problem.\n",
    "\n",
    "The MIP solution approach proposed by Cohen [1] is an improvement over the \n",
    "traditional myopic\n",
    "approach of picking the customers that have the largest expected value for a particular product because it\n",
    "produces a globally optimal solution \n",
    "from the viewpoint of the bank and allows for the effective implementation of business constraints across customers and business units. The approach accounts for limited resources and other business constraints. \n",
    "\n",
    "We assume that the estimates for customer/offer expected incremental profit, costs, and business constraints serve as inputs to the marketing campaign optimization approach. The optimization phase is independent of the construction of these inputs. \n",
    "\n",
    "The MIP approach\n",
    "involves \n",
    "a tactical and an operational problem. \n",
    "For the tactical problem, we aggregate customers based on the individual expected profit parameters.  The estimated individual expected profit can be determined with data science techniques such as predictive response models. The key idea is to cluster the estimated individual expected profits and then consider the cluster centroids as representative of the data for all the individual customers within a single cluster. This aggregation enables the problem to be formulated as a linear programming problem so that rather than assigning offers to individual customers, the model identifies proportions within each cluster for each product offer that maximizes the marketing campaign return on investment while considering the business constraints. Typically, the number of customers in a cluster will be in the hundreds of thousands, which is the main decision variable of the tactical problem, consequently these variables can be considered as a continuous; therefore, the linear programming approach is justified.\n",
    "\n",
    "The operational problem can be formulated as a MIP model, where the estimated individual expected profits and the output of the tactical model can be used as inputs to assign *products offers* to individual customers of each cluster in such a way that the  total marketing campaign return on investment is maximized."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Tactical Model Formulation\n",
    "\n",
    "### Sets and Indices\n",
    "$k \\in K$: Index and set of clusters.\n",
    "\n",
    "$j \\in J$: Index and set of products.\n",
    "\n",
    "### Parameters\n",
    "$\\pi_{k,j}$: Expected profit to the bank from the offer of product $j \\in J$ to an average customer of cluster $k \\in K$.\n",
    "\n",
    "$\\nu_{k,j}$: Average variable cost associated with the offer of product  $j \\in J$ to an average customer of cluster $k \\in K$.\n",
    "  \n",
    "$N_{k}$: Number of customers in cluster $k \\in K$.\n",
    "\n",
    "$Q_{j}$: Minimum number of offers of product $j \\in J$. \n",
    "\n",
    "$R$: Corporate hurdle rate. This hurdle rate is used for the ROI calculation of the marketing campaign.\n",
    "\n",
    "$B$: Marketing campaign budget.\n",
    "\n",
    "$M$: Big M penalty. This penalty is associated with corrections on the budget that are necessary to satisfy other business constraints.\n",
    " \n",
    "\n",
    "### Decision Variables\n",
    "$y_{k,j} \\geq 0$: Number of customers in cluster $k \\in K$ that are offered product $j \\in J$.\n",
    "\n",
    "$z \\geq 0$: Increase in budget in order to have a feasible campaign.\n",
    "\n",
    "### Objective Function\n",
    "- **Total profit**. Maximize total expected profit from marketing campaign and heavily penalize any correction to the budget.\n",
    "\n",
    "\\begin{equation}\n",
    "\\text{Max} \\quad Z = \\sum_{k \\in K} \\sum_{j \\in J} \\pi_{k,j} \\cdot y_{k,j} - M \\cdot z\n",
    "\\tag{0}\n",
    "\\end{equation}\n",
    "\n",
    "### Constraints\n",
    "\n",
    "- **Number of offers**. Maximum number of offers of products for each cluster is limited by the number of customers in the cluster.\n",
    "\n",
    "\\begin{equation}\n",
    "\\sum_{j \\in J} y_{k,j} \\leq N_{k} \\quad \\forall k \\in K\n",
    "\\tag{1}\n",
    "\\end{equation}\n",
    "\n",
    "- **Budget**. The marketing campaign budget constraint enforces that the total cost of the campaign should be less than the budget campaign. There is the possibility of increasing the budget to ensure the feasibility of the model, the minimum number of offers for all the product may require this increase in the budget.\n",
    "\n",
    "\\begin{equation}\n",
    "\\sum_{k \\in K} \\sum_{j \\in J} \\nu_{k,j} \\cdot y_{k,j} \\leq B + z\n",
    "\\tag{2}\n",
    "\\end{equation}\n",
    "\n",
    "- **Offers limit**. Minimum number of offers of each product.\n",
    "\n",
    "\\begin{equation}\n",
    "\\sum_{k \\in K} y_{k,j} \\geq Q_{j}  \\quad \\forall j \\in J\n",
    "\\tag{3}\n",
    "\\end{equation}\n",
    "\n",
    "- **ROI**. The minimum ROI constraint ensures that the ratio of total profits over cost is at least one plus the corporate hurdle rate.\n",
    "\n",
    "\\begin{equation}\n",
    "\\sum_{k \\in K} \\sum_{j \\in J} \\pi_{k,j} \\cdot y_{k,j} \\geq (1+R) \\cdot \\sum_{k \\in K} \\sum_{j \\in J} \\nu_{k,j} \\cdot y_{k,j}\n",
    "\\tag{4}\n",
    "\\end{equation}\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Operational Model Formulation\n",
    "\n",
    "Once the optimal values $y_{k,j}$, for all $j \\in J$ and $k \\in K$, of the Tactical model have been found, we should determine which individual customers in cluster $k$ should get an offer of a product. Suppose that for a given cluster $k \\in K$, the allocation of offers of product $j_1$ and $j_2$ are positive, i.e. $y_{k,j_1} > 0$ and $y_{k,j_2} > 0$. Then, $y_{k,j_1}$ and $y_{k,j_2}$ of customers in cluster $k$ must be offered product $j_1$ and $j_2$, respectively. The optimal way to do that is to solve an assignment problem using the estimated expected profit for the individual customers and not the one for clusters.\n",
    "\n",
    "We now provide a formulation of the operational problem.\n",
    "\n",
    "### Sets and Indices\n",
    "$i \\in I^{k}$: Index and set of customers in cluster $k \\in K$.\n",
    "\n",
    "$j \\in J^{k}$: Index and subset of products offered to customers in cluster $k \\in K$ , where $J^{k} = \\{ j \\in J: y_{k,j} > 0 \\}$ .\n",
    "\n",
    "### Parameters\n",
    "\n",
    "$r_{k,i,j}$: Expected individual profit of customer $i \\in I^{k}$ from  offer of product $j \\in J^{k}$. \n",
    "\n",
    "$Y_{k,j} = \\lfloor y_{k,j} \\rfloor $: Number of customers in cluster k that will get an offer of product $j \\in J^{k}$.\n",
    "\n",
    "### Decision Variables\n",
    "$x_{k,i,j} \\in \\{0,1 \\}$: This variable is equal to 1, if product $j \\in J^{k}$  is offered to customer $i \\in I^{k}$, and 0 otherwise.\n",
    "\n",
    "\n",
    "\n",
    "### Objective Function\n",
    "- **Total profit**. Maximize total individual profit.\n",
    "\n",
    "\\begin{equation}\n",
    "\\text{Max} \\quad Z = \\sum_{k \\in K} \\sum_{i \\in I^{k}} \\sum_{j \\in J^{k}} r_{k,i,j} \\cdot x_{k,i,j}\n",
    "\\tag{0}\n",
    "\\end{equation}\n",
    "\n",
    "\n",
    "### Constraints\n",
    "\n",
    "- **Product offers**. Allocate offers of a product to customers of each cluster.\n",
    "\n",
    "\\begin{equation}\n",
    "\\sum_{i \\in  I^{k}}  x_{k,i,j} = Y_{k,j}  \\quad \\forall j \\in J^{k}, k \\in K\n",
    "\\tag{1}\n",
    "\\end{equation}\n",
    "\n",
    "\n",
    "- **Offers limit**. At most one product may be offered to a customer of a cluster.\n",
    "\n",
    "\\begin{equation}\n",
    "\\sum_{j \\in J^{k}} x_{k,i,j} \\leq 1 \\quad \\forall i \\in I^{k}, k \\in K\n",
    "\\tag{2}\n",
    "\\end{equation}\n",
    "\n",
    "- **Binary constraints**. Either a product offer is given to a customer of cluster k or not.\n",
    "\n",
    "\\begin{equation}\n",
    "x_{k,i,j} \\in \\{0,1 \\} \\quad \\forall i \\in I^{k},  j \\in J^{k}, k \\in K\n",
    "\\tag{3}\n",
    "\\end{equation}\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Problem Instance\n",
    "\n",
    "We consider two products, ten customers, and two clusters of customers. The corporate hurdle-rate is twenty percent.\n",
    "\n",
    "### Tactical problem data\n",
    "\n",
    "The following table defines the expected profit of an average customer in each cluster when offered a product.\n",
    "\n",
    "| <i></i> | Product 1 | Product 2 |\n",
    "| --- | --- |  --- |\n",
    "| cluster 1 | $\\$2,000$ | $\\$1,000$ |\n",
    "| cluster 2 | $\\$3,000$ | $\\$2,000$ |\n",
    "\n",
    "The expected cost of offering a product to an average customer in a cluster is determined by the following table.\n",
    "\n",
    "| <i></i> | Product 1 | Product 2 |\n",
    "| --- | --- |  --- |\n",
    "| cluster 1 | $\\$200$ | $\\$100$ |\n",
    "| cluster 2 | $\\$300$ | $\\$200$ |\n",
    "\n",
    "The budget available for the marketing campaign is $\\$200$.\n",
    "\n",
    "The number of customers in each cluster is given by the following table.\n",
    "\n",
    "| <i></i> | Num. Customers | \n",
    "| --- | --- |\n",
    "| cluster 1 | 5 |\n",
    "| cluster 2 | 5 | \n",
    "\n",
    "The minimum number of offers of each product is provided in the following table,\n",
    "\n",
    "| <i></i> | Min Offers | \n",
    "| --- | --- |\n",
    "| product 1 | 2 |\n",
    "| product 2 | 2 | \n",
    "\n",
    "### Operational problem data\n",
    "\n",
    "The following table shows the expected profit of each customer in each cluster when offered a product.\n",
    "\n",
    "| <i></i> | Product 1 | Product 2 |\n",
    "| --- | --- |  --- |\n",
    "| cluster 1, customer 1 | $\\$2,050$ | $\\$1,050$ |\n",
    "| cluster 1, customer 2 | $\\$1,950$ | $\\$950$ |\n",
    "| cluster 1, customer 3 | $\\$2,000$ | $\\$1,000$ |\n",
    "| cluster 1, customer 4 | $\\$2,100$ | $\\$1,100$ |\n",
    "| cluster 1, customer 5 | $\\$1,900$ | $\\$900$ |\n",
    "| cluster 2, customer 6 | $\\$3,000$ | $\\$2,000$ |\n",
    "| cluster 2, customer 7 | $\\$2,900$ | $\\$1,900$ |\n",
    "| cluster 2, customer 8 | $\\$3,050$ | $\\$2,050$ |\n",
    "| cluster 2, customer 9 | $\\$3,100$ | $\\$2,100$ |\n",
    "| cluster 2, customer 10 | $\\$2,950$ | $\\$1,950$ |\n",
    "\n",
    "The following table shows the cost of offering a product to a customer in a cluster.\n",
    "\n",
    "| <i></i> | Product 1 | Product 2 |\n",
    "| --- | --- |  --- |\n",
    "| cluster 1, customer 1 | $\\$205$ | $\\$105$ |\n",
    "| cluster 1, customer 2 | $\\$195$ | $\\$95$ |\n",
    "| cluster 1, customer 3 | $\\$200$ | $\\$100$ |\n",
    "| cluster 1, customer 4 | $\\$210$ | $\\$110$ |\n",
    "| cluster 1, customer 5 | $\\$190$ | $\\$90$ |\n",
    "| cluster 2, customer 6 | $\\$300$ | $\\$200$ |\n",
    "| cluster 2, customer 7 | $\\$290$ | $\\$190$ |\n",
    "| cluster 2, customer 8 | $\\$305$ | $\\$205$ |\n",
    "| cluster 2, customer 9 | $\\$310$ | $\\$210$ |\n",
    "| cluster 2, customer 10 | $\\$295$ | $\\$195$ |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Python Implementation\n",
    "\n",
    "We now import the Gurobi Python Module. Then, we initialize the data structures with the given data."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%pip install gurobipy"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import gurobipy as gp\n",
    "from gurobipy import GRB\n",
    "\n",
    "# tested with Gurobi v9.0.0 and Python 3.7.0\n",
    "\n",
    "### SETS\n",
    "\n",
    "products = ['p1', 'p2']\n",
    "clusters = ['k1', 'k2']"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Expected profit\n",
    "\n",
    "The following tables shows the expected profit of a customer in each cluster when offered a product.\n",
    "\n",
    "| <i></i> | Product 1 | Product 2 |\n",
    "| --- | --- |  --- |\n",
    "| cluster 1 | $\\$2,000$ | $\\$1,000$ |\n",
    "| cluster 2 | $\\$3,000$ | $\\$2,000$ |"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "### Parameters\n",
    "\n",
    "# Expected profit\n",
    "cp, expected_profit = gp.multidict({\n",
    "    ('k1', 'p1'): 2000,\n",
    "    ('k1', 'p2'): 1000,\n",
    "    ('k2', 'p1'): 3000,\n",
    "    ('k2', 'p2'): 2000\n",
    "})\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Expected cost\n",
    "\n",
    "The expected cost of offering a product to a customer in a cluster is shown in the following table.\n",
    "\n",
    "| <i></i> | Product 1 | Product 2 |\n",
    "| --- | --- |  --- |\n",
    "| cluster 1 | $\\$200$ | $\\$100$ |\n",
    "| cluster 2 | $\\$300$ | $\\$200$ |"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Expected cost\n",
    "\n",
    "cp, expected_cost = gp.multidict({\n",
    "    ('k1', 'p1'): 200,\n",
    "    ('k1', 'p2'): 100,\n",
    "    ('k2', 'p1'): 300,\n",
    "    ('k2', 'p2'): 200\n",
    "})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Number of customers\n",
    "The number of customers in each cluster can be seen in the following table.\n",
    "\n",
    "| <i></i> | Num. Customers | \n",
    "| --- | --- |\n",
    "| cluster 1 | 5 |\n",
    "| cluster 2 | 5 | "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Num of customers in each cluster\n",
    "\n",
    "clusters, number_customers = gp.multidict({\n",
    "    ('k1'): 5,\n",
    "    ('k2'): 5\n",
    "})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Minimum number of offers\n",
    "\n",
    "The minimum number of offers of each product is provided in the following table.\n",
    "\n",
    "| <i></i> | Min Offers | \n",
    "| --- | --- |\n",
    "| product 1 | 2 |\n",
    "| product 2 | 2 | "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Minimum number offers for each product\n",
    "\n",
    "products, min_offers = gp.multidict({\n",
    "    ('p1'): 2,\n",
    "    ('p2'): 2\n",
    "})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Scalars\n",
    "\n",
    "The corporate hurdle-rate is twenty percent ($R = 0.20$).\n",
    "\n",
    "The budget available for the marketing campaign is $\\$200$.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Scalars\n",
    "\n",
    "R = 0.20\n",
    "\n",
    "#tight budget\n",
    "budget = 200 "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Tactical Model Formulation\n",
    " \n",
    "\n",
    "### Decision Variables\n",
    "$y_{k,j} \\geq 0$: Number of customers in cluster $k \\in K$ that are offered product $j \\in J$.\n",
    "\n",
    "$z \\geq 0$: Increase in budget in order to have a feasible campaign."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Using license file c:\\gurobi\\gurobi.lic\n"
     ]
    }
   ],
   "source": [
    "# Declare and initialize model\n",
    "mt = gp.Model('Tactical')\n",
    "\n",
    "### Decisions variables\n",
    "\n",
    "# Allocation of product offers to customers in clusters.\n",
    "\n",
    "y = mt.addVars(cp, name=\"allocate\")\n",
    "\n",
    "# Budget correction\n",
    "\n",
    "z = mt.addVar(name=\"budget_correction\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Constraints\n",
    "\n",
    "- **Number of offers**. Maximum number of offers of products for each cluster.\n",
    "\n",
    "\\begin{equation}\n",
    "\\sum_{j \\in J} y_{k,j} \\leq N_{k} \\quad \\forall k \\in K\n",
    "\\tag{1}\n",
    "\\end{equation}\n",
    "\n",
    "Where\n",
    "\n",
    "$y_{k,j} \\geq 0$: Number of customers in cluster $k \\in K$ that are offered product $j \\in J$.\n",
    "\n",
    "$N_{k}$: Number of customers in cluster $k \\in K$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "### Constraints\n",
    "\n",
    "# Constraint on number of offers at each cluster\n",
    "\n",
    "maxOffers_cons = mt.addConstrs((y.sum(k,'*') <= number_customers[k]  for k in clusters), name='maxOffers')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Constraints\n",
    "\n",
    "- **Budget**. The marketing campaign budget constraint enforces that the total cost of the campaign should be less than the budget campaign. There is the possibility of increasing the budget to ensure the feasibility of the model, the minimum number of offers for all the product may require this increase in the budget.\n",
    "\n",
    "\\begin{equation}\n",
    "\\sum_{k \\in K} \\sum_{j \\in J} \\nu_{k,j} \\cdot y_{k,j} \\leq B + z\n",
    "\\tag{2}\n",
    "\\end{equation}\n",
    "\n",
    "Where\n",
    "\n",
    "$y_{k,j} \\geq 0$: Number of customers in cluster $k \\in K$ that are offered product $j \\in J$.\n",
    "\n",
    "$z \\geq 0$: Increase in budget in order to have a feasible campaign.\n",
    "\n",
    "$\\nu_{k,j}$: Average variable cost associated with the offer of product  $j \\in J$ to an average customer of cluster $k \\in K$.\n",
    "\n",
    "$B$: Marketing campaign budget."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Budget constraint\n",
    "\n",
    "budget_con = mt.addConstr((y.prod(expected_cost) - z <= budget), name='budget')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Constraints\n",
    "\n",
    "- **Offers limit**. Minimum number of offers of each product.\n",
    "\n",
    "\\begin{equation}\n",
    "\\sum_{k \\in K} y_{k,j} \\geq Q_{j}  \\quad \\forall j \\in J\n",
    "\\tag{3}\n",
    "\\end{equation}\n",
    "\n",
    "Where\n",
    "\n",
    "$y_{k,j} \\geq 0$: Number of customers in cluster $k \\in K$ that are offered product $j \\in J$.\n",
    "\n",
    "$Q_{j}$: Minimum number of offers of product $j \\in J$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Constraints on min number of offers of each product\n",
    "\n",
    "minOffers_cons = mt.addConstrs( (y.sum('*',j) >= min_offers[j] for j in products), name='min_offers')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Constraints\n",
    "\n",
    "\n",
    "- **ROI**. The minimum ROI constraint ensures that the ratio of total profits over cost is at least  one plus the corporate hurdle rate.\n",
    "\n",
    "\\begin{equation}\n",
    "\\sum_{k \\in K} \\sum_{j \\in J} \\pi_{k,j} \\cdot y_{k,j} \\geq (1+R) \\cdot \\sum_{k \\in K} \\sum_{j \\in J} \\nu_{k,j} \\cdot y_{k,j}\n",
    "\\tag{4}\n",
    "\\end{equation}\n",
    "\n",
    "Where\n",
    "\n",
    "$y_{k,j} \\geq 0$: Number of customers in cluster $k \\in K$ that are offered product $j \\in J$.\n",
    "\n",
    "$\\pi_{k,j}$: Expected profit to the bank from the offer of product $j \\in J$ to an average customer of cluster $k \\in K$.\n",
    "\n",
    "$\\nu_{k,j}$: Average variable cost associated with the offer of product  $j \\in J$ to an average customer of cluster $k \\in K$.\n",
    "\n",
    "$R$: Corporate hurdle rate."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Constraint to ensure minimum ROI\n",
    "\n",
    "ROI_con = mt.addConstr((y.prod(expected_profit) - (1 + R)*y.prod(expected_cost) >= 0), name='ROI')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Objective Function\n",
    "- **Total profit**. Maximize total expected profit from marketing campaign and heavily penalize any correction to the budget.\n",
    "\n",
    "\\begin{equation}\n",
    "\\text{Max} \\quad Z = \\sum_{k \\in K} \\sum_{j \\in J} \\pi_{k,j} \\cdot y_{k,j} - M \\cdot z\n",
    "\\tag{0}\n",
    "\\end{equation}\n",
    "\n",
    "Where\n",
    "\n",
    "$y_{k,j} \\geq 0$: Number of customers in cluster $k \\in K$ that are offered product $j \\in J$.\n",
    "\n",
    "$z \\geq 0$: Increase in budget in order to have a feasible campaign.\n",
    "\n",
    "$\\pi_{k,j}$: Expected profit to the bank from the offer of product $j \\in J$ to an average customer of cluster $k \\in K$.\n",
    "\n",
    "**Note:** The value of $M$ should be higher than any of the expected profits to ensure that the budget is increased only when the model is infeasible if this parameter is not increased."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "### Objective function\n",
    "\n",
    "# Maximize total expected profit\n",
    "\n",
    "M = 10000\n",
    "\n",
    "mt.setObjective(y.prod(expected_profit) -M*z, GRB.MAXIMIZE)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Verify model formulation\n",
    "\n",
    "mt.write('tactical.lp')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Gurobi Optimizer version 9.1.0 build v9.1.0rc0 (win64)\n",
      "Thread count: 4 physical cores, 8 logical processors, using up to 8 threads\n",
      "Optimize a model with 6 rows, 5 columns and 17 nonzeros\n",
      "Model fingerprint: 0x1eac2f22\n",
      "Coefficient statistics:\n",
      "  Matrix range     [1e+00, 3e+03]\n",
      "  Objective range  [1e+03, 1e+04]\n",
      "  Bounds range     [0e+00, 0e+00]\n",
      "  RHS range        [2e+00, 2e+02]\n",
      "Presolve removed 1 rows and 0 columns\n",
      "Presolve time: 0.01s\n",
      "Presolved: 5 rows, 5 columns, 13 nonzeros\n",
      "\n",
      "Iteration    Objective       Primal Inf.    Dual Inf.      Time\n",
      "       0    2.5000000e+04   8.787500e+01   0.000000e+00      0s\n",
      "       4   -3.9940000e+06   0.000000e+00   0.000000e+00      0s\n",
      "\n",
      "Solved in 4 iterations and 0.01 seconds\n",
      "Optimal objective -3.994000000e+06\n"
     ]
    }
   ],
   "source": [
    "# Run optimization engine\n",
    "\n",
    "mt.optimize()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "Optimal allocation of product offers to clusters.\n",
      "___________________________________________________\n",
      "The number of customers in cluster k1 that gets an offer of product p1 is: 2.0\n",
      "The number of customers in cluster k1 that gets an offer of product p2 is: 2.0\n",
      "\n",
      "The increase correction in the campaign budget is $400.00.\n",
      "\n",
      "Financial reports.\n",
      "___________________________________________________\n",
      "Optimal total expected profit is $6,000.00.\n",
      "Optimal total expected cost is $600.00 with a budget of $200.00 and an extra amount of $400.00.\n",
      "Optimal ROI is 1000.0% with a minimum ROI of  120.0%.\n"
     ]
    }
   ],
   "source": [
    "### Output Reports\n",
    "\n",
    "# Optimal allocation of product offers to clusters\n",
    "\n",
    "total_expected_profit = 0\n",
    "total_expected_cost = 0\n",
    "\n",
    "print(\"\\nOptimal allocation of product offers to clusters.\")\n",
    "print(\"___________________________________________________\")\n",
    "for k,p in cp:\n",
    "    if y[k,p].x > 1e-6:\n",
    "        #print(y[k,p].varName, y[k,p].x)\n",
    "        print(f\"The number of customers in cluster {k} that gets an offer of product {p} is: {y[k,p].x}\")\n",
    "        total_expected_profit += expected_profit[k,p]*y[k,p].x\n",
    "        total_expected_cost += expected_cost[k,p]*y[k,p].x\n",
    "\n",
    "increased_budget = '${:,.2f}'.format(z.x)\n",
    "print(f\"\\nThe increase correction in the campaign budget is {increased_budget}.\")\n",
    "\n",
    "# Financial reports\n",
    "\n",
    "optimal_ROI = round(100*total_expected_profit/total_expected_cost,2)\n",
    "min_ROI = round(100*(1+R),2)\n",
    "\n",
    "money_expected_profit = '${:,.2f}'.format(total_expected_profit)\n",
    "money_expected_cost = '${:,.2f}'.format(total_expected_cost)\n",
    "money_budget = '${:,.2f}'.format(budget)\n",
    "\n",
    "print(f\"\\nFinancial reports.\")\n",
    "print(\"___________________________________________________\")\n",
    "print(f\"Optimal total expected profit is {money_expected_profit}.\")\n",
    "print(f\"Optimal total expected cost is {money_expected_cost} with a budget of {money_budget} and an extra amount of {increased_budget}.\")\n",
    "print(f\"Optimal ROI is {optimal_ROI}% with a minimum ROI of  {min_ROI}%.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Analysis\n",
    "\n",
    "The cost of allocating products to clusters required an increase in the budget of $\\$400$. The total expected profit is \n",
    "$\\$6,000$. The total expected cost is $\\$600$ which is equal to the original budget of $\\$200$ plus the increase of $\\$400$. The expected ROI is 1,000 % which is much higher than the minimum ROI required."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Operational Model Formulation\n",
    "\n",
    "### Customer expected profit"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "### Sets\n",
    "\n",
    "customers = ['c1', 'c2','c3','c4','c5','c6','c7','c8','c9','c10']\n",
    "\n",
    "### Parameters\n",
    "\n",
    "# Expected profit from a product offering for each customer in each cluster\n",
    "ccp, customer_profit = gp.multidict({\n",
    "    ('k1', 'c1', 'p1'): 2050,\n",
    "    ('k1', 'c1', 'p2'): 1050,\n",
    "    ('k1', 'c2', 'p1'): 1950,\n",
    "    ('k1', 'c2', 'p2'): 950,\n",
    "    ('k1', 'c3', 'p1'): 2000,\n",
    "    ('k1', 'c3', 'p2'): 1000,\n",
    "    ('k1', 'c4', 'p1'): 2100,\n",
    "    ('k1', 'c4', 'p2'): 1100,\n",
    "    ('k1', 'c5', 'p1'): 1900,\n",
    "    ('k1', 'c5', 'p2'): 900,\n",
    "    ('k2', 'c6', 'p1'): 3000,\n",
    "    ('k2', 'c6', 'p2'): 2000,\n",
    "    ('k2', 'c7', 'p1'): 2900,\n",
    "    ('k2', 'c7', 'p2'): 1900,\n",
    "    ('k2', 'c8', 'p1'): 3050,\n",
    "    ('k2', 'c8','p2'): 2050,\n",
    "    ('k2', 'c9', 'p1'): 3100,\n",
    "    ('k2', 'c9', 'p2'): 3100,\n",
    "    ('k2', 'c10', 'p1'): 2950,\n",
    "    ('k2', 'c10', 'p2'): 2950   \n",
    "})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Customer offering cost"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Customer cost of offering a product at a cluster\n",
    "\n",
    "ccp, customer_cost = gp.multidict({\n",
    "    ('k1', 'c1', 'p1'): 205,\n",
    "    ('k1', 'c1', 'p2'): 105,\n",
    "    ('k1', 'c2', 'p1'): 195,\n",
    "    ('k1', 'c2', 'p2'): 95,\n",
    "    ('k1', 'c3', 'p1'): 200,\n",
    "    ('k1', 'c3', 'p2'): 100,\n",
    "    ('k1', 'c4', 'p1'): 210,\n",
    "    ('k1', 'c4', 'p2'): 110,\n",
    "    ('k1', 'c5', 'p1'): 190,\n",
    "    ('k1', 'c5', 'p2'): 90,\n",
    "    ('k2', 'c6', 'p1'): 300,\n",
    "    ('k2', 'c6', 'p2'): 200,\n",
    "    ('k2', 'c7', 'p1'): 290,\n",
    "    ('k2', 'c7', 'p2'): 190,\n",
    "    ('k2', 'c8', 'p1'): 305,\n",
    "    ('k2', 'c8','p2'): 205,\n",
    "    ('k2', 'c9', 'p1'): 310,\n",
    "    ('k2', 'c9', 'p2'): 310,\n",
    "    ('k2', 'c10', 'p1'): 295,\n",
    "    ('k2', 'c10', 'p2'): 295   \n",
    "})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Operational Model Formulation\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Decision Variables\n",
    "$x_{k,i,j} \\in \\{0,1 \\}$: This variable is equal to 1, if product $j \\in J^{k}$  is offered to customer $i \\in I^{k}$, and 0 otherwise."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Declare and initialize model\n",
    "mo = gp.Model('Operational')\n",
    "\n",
    "### Decision variables\n",
    "\n",
    "x = mo.addVars(ccp, vtype=GRB.BINARY, name=\"assign\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Constraints\n",
    "\n",
    "- **Product offers**. Allocate offers of a product to customers of each cluster.\n",
    "\n",
    "\\begin{equation}\n",
    "\\sum_{i \\in  I^{k}}  x_{k,i,j} = Y_{k,j}  \\quad \\forall j \\in J^{k}, k \\in K\n",
    "\\tag{1}\n",
    "\\end{equation}\n",
    "\n",
    "Where\n",
    "\n",
    "$x_{k,i,j} \\in \\{0,1 \\}$: This variable is equal to 1, if product $j \\in J^{k}$  is offered to customer $i \\in I^{k}$, and 0 otherwise.\n",
    "\n",
    "$Y_{k,j} = \\lfloor y_{k,j} \\rfloor $: Number of customers in cluster k that will get an offer of product $j \\in J^{k}$.\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Product offers constraint\n",
    "\n",
    "productOffers = {}\n",
    "\n",
    "for k in clusters:\n",
    "    for j in products:\n",
    "            productOffers[k,j] = mo.addConstr(gp.quicksum(x[k,i,j] for kk,i,jj in ccp if (kk ==k and jj == j)) == \n",
    "                                              int(y[k,j].x), name='prodOffers_' + str(k) + ',' + str(j) )"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "### Constraints\n",
    "\n",
    "\n",
    "- **Offers limit**. At most one product may be offered to a customer of a cluster.\n",
    "\n",
    "\\begin{equation}\n",
    "\\sum_{j \\in J^{k}} x_{k,i,j} \\leq 1 \\quad \\forall i \\in I^{k}, k \\in K\n",
    "\\tag{2}\n",
    "\\end{equation}\n",
    "\n",
    "Where\n",
    "\n",
    "$x_{k,i,j} \\in \\{0,1 \\}$: This variable is equal to 1, if product $j \\in J^{k}$  is offered to customer $i \\in I^{k}$, and 0 otherwise."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [],
   "source": [
    "# limit on the number of offers to each customer in a cluster.\n",
    "\n",
    "ki = [('k1', 'c1'), \n",
    "      ('k1', 'c2'), \n",
    "      ('k1', 'c3'),\n",
    "      ('k1', 'c4'), \n",
    "      ('k1', 'c5'), \n",
    "      ('k2', 'c6'), \n",
    "      ('k2', 'c7'), \n",
    "      ('k2', 'c8'), \n",
    "      ('k2', 'c9'), \n",
    "      ('k2', 'c10')]\n",
    "\n",
    "customerOffers = {}\n",
    "\n",
    "for k,i in ki:\n",
    "    customerOffers[k,i] = mo.addConstr(gp.quicksum(x[k,i,j] for kk,ii,j in ccp if (kk == k and ii == i) ) <= 1, \n",
    "                                          name ='custOffers_' + str(k) + ',' + str(i) )"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Objective Function\n",
    "\n",
    "- **Total profit**. Maximize total individual expected profit.\n",
    "\n",
    "\\begin{equation}\n",
    "\\text{Max} \\quad Z = \\sum_{k \\in K}  \\sum_{i \\in I^{k}} \\sum_{j \\in J^{k}} r_{k,i,j} \\cdot x_{k,i,j}\n",
    "\\tag{0}\n",
    "\\end{equation}\n",
    "\n",
    "Where\n",
    "\n",
    "$x_{k,i,j} \\in \\{0,1 \\}$: This variable is equal to 1, if product $j \\in J^{k}$  is offered to customer $i \\in I^{k}$, and 0 otherwise.\n",
    "\n",
    "$r_{k,i,j}$: Expected individual profit of customer $i \\in I^{k}$ from  offer of product $j \\in J^{k}$. \n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [],
   "source": [
    "### Objective function\n",
    "\n",
    "# Maximoze total profit\n",
    "\n",
    "mo.setObjective(x.prod(customer_profit), GRB.MAXIMIZE)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Gurobi Optimizer version 9.1.0 build v9.1.0rc0 (win64)\n",
      "Thread count: 4 physical cores, 8 logical processors, using up to 8 threads\n",
      "Optimize a model with 14 rows, 20 columns and 40 nonzeros\n",
      "Model fingerprint: 0xe1e9d99f\n",
      "Variable types: 0 continuous, 20 integer (20 binary)\n",
      "Coefficient statistics:\n",
      "  Matrix range     [1e+00, 1e+00]\n",
      "  Objective range  [9e+02, 3e+03]\n",
      "  Bounds range     [1e+00, 1e+00]\n",
      "  RHS range        [1e+00, 2e+00]\n",
      "Found heuristic solution: objective 6050.0000000\n",
      "Presolve removed 7 rows and 10 columns\n",
      "Presolve time: 0.00s\n",
      "Presolved: 7 rows, 10 columns, 20 nonzeros\n",
      "Variable types: 0 continuous, 10 integer (10 binary)\n",
      "\n",
      "Root relaxation: objective 6.100000e+03, 2 iterations, 0.00 seconds\n",
      "\n",
      "    Nodes    |    Current Node    |     Objective Bounds      |     Work\n",
      " Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time\n",
      "\n",
      "*    0     0               0    6100.0000000 6100.00000  0.00%     -    0s\n",
      "\n",
      "Explored 0 nodes (2 simplex iterations) in 0.02 seconds\n",
      "Thread count was 8 (of 8 available processors)\n",
      "\n",
      "Solution count 2: 6100 6050 \n",
      "\n",
      "Optimal solution found (tolerance 1.00e-04)\n",
      "Best objective 6.100000000000e+03, best bound 6.100000000000e+03, gap 0.0000%\n"
     ]
    }
   ],
   "source": [
    "# Verify model formulation\n",
    "\n",
    "mo.write('operational.lp')\n",
    "\n",
    "# Run optimization engine\n",
    "\n",
    "mo.optimize()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "Optimal assignment of product offers to customers.\n",
      "___________________________________________________\n",
      "Customer c1 in cluster k1 gets an offer of product p2:\n",
      "The expected profit is $1,050.00 at a cost of $105.00\n",
      "Customer c2 in cluster k1 gets an offer of product p2:\n",
      "The expected profit is $950.00 at a cost of $95.00\n",
      "Customer c3 in cluster k1 gets an offer of product p1:\n",
      "The expected profit is $2,000.00 at a cost of $200.00\n",
      "Customer c4 in cluster k1 gets an offer of product p1:\n",
      "The expected profit is $2,100.00 at a cost of $210.00\n",
      "___________________________________________________\n",
      "Number of assignments in cluster k1 is 4\n",
      "___________________________________________________\n",
      "___________________________________________________\n",
      "Number of assignments in cluster k2 is 0\n",
      "___________________________________________________\n",
      "\n",
      "\n",
      "Financial reports.\n",
      "___________________________________________________\n",
      "Optimal total customers profit is $6,100.00.\n",
      "Optimal total customers cost is $610.00 with a budget of $200.00 and an extra amount of $400.00.\n",
      "Optimal ROI is 1000.0% with a minimum ROI of  120.0%.\n"
     ]
    }
   ],
   "source": [
    "### Output Reports\n",
    "\n",
    "# Optimal assignment of product offers to customers\n",
    "\n",
    "total_customer_profit = 0\n",
    "total_customer_cost = 0\n",
    "\n",
    "kvalue = None\n",
    "first = True\n",
    "num_assignments = 0\n",
    "\n",
    "print(\"\\nOptimal assignment of product offers to customers.\")\n",
    "print(\"___________________________________________________\")\n",
    "for k,i,j in ccp:\n",
    "    if k != kvalue:\n",
    "        prevk = kvalue\n",
    "        kvalue = k\n",
    "        if not first:\n",
    "            print(\"___________________________________________________\")\n",
    "            print(f\"Number of assignments in cluster {prevk} is {num_assignments}\")\n",
    "            print(\"___________________________________________________\")\n",
    "            num_assignments = 0\n",
    "        if first:\n",
    "            first = False\n",
    "    if x[k,i,j].x > 0.5:\n",
    "        #print(x[k,i,j].varName, x[k,i,j].x)\n",
    "        profit = '${:,.2f}'.format(customer_profit[k,i,j])\n",
    "        cost = '${:,.2f}'.format(customer_cost[k,i,j])\n",
    "        print(f\"Customer {i} in cluster {k} gets an offer of product {j}:\")\n",
    "        print(f\"The expected profit is {profit} at a cost of {cost}\")\n",
    "        total_customer_profit += customer_profit[k,i,j]*x[k,i,j].x\n",
    "        total_customer_cost += customer_cost[k,i,j]*x[k,i,j].x\n",
    "        num_assignments += 1\n",
    "print(\"___________________________________________________\")\n",
    "print(f\"Number of assignments in cluster {kvalue} is {num_assignments}\")\n",
    "print(\"___________________________________________________\\n\")\n",
    "        \n",
    "# Financial reports\n",
    "\n",
    "customers_ROI = round(100*total_customer_profit/total_customer_cost,2)\n",
    "\n",
    "money_customers_profit = '${:,.2f}'.format(total_customer_profit)\n",
    "money_customers_cost = '${:,.2f}'.format(total_customer_cost)\n",
    "\n",
    "print(f\"\\nFinancial reports.\")\n",
    "print(\"___________________________________________________\")\n",
    "print(f\"Optimal total customers profit is {money_customers_profit}.\")\n",
    "print(f\"Optimal total customers cost is {money_customers_cost} with a budget of {money_budget} and an extra amount of {increased_budget}.\")\n",
    "print(f\"Optimal ROI is {customers_ROI}% with a minimum ROI of  {min_ROI}%.\")\n",
    "        \n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Analysis\n",
    "Each customer got, at most, one product offer.  Product p2 is offered to customers c1 and c2, and product p1 is offered to customers c3 and c4. Products p1 and p2 are offerred to at least two customers -this is a constraint from the tactical model. Observe that to ensure these hard business constraints, the budget needs to be increased by $\\$400$\n",
    "\n",
    "The cost of assigning  products to customers is $\\$610$, which slightly violates the total budget available of $\\$600$. The total customers profit is $\\$6,100$. The  ROI is 1,000 %, which is much higher than the minimum ROI required.\n",
    "\n",
    "If the total available budget needs to be enforced, the following constraint can be added to the operational model:\n",
    "\n",
    "- **Budget**. Enforce budget constraint.\n",
    "\n",
    "\\begin{equation}\n",
    "\\sum_{k \\in K}  \\sum_{i \\in I^{k}} \\sum_{j \\in J^{k}} c_{k,i,j} \\cdot x_{k,i,j} \\leq B'\n",
    "\\tag{4}\n",
    "\\end{equation}\n",
    "\n",
    "The new budget is the original budget plus the correction, that is  $B' = B + z$"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Scenario 1\n",
    "Enforce total budget available constraint. In this case, the operational model is:\n",
    "\n",
    "### Objective function\n",
    "\n",
    "- **Total profit**. Maximize total individual expected profit.\n",
    "\n",
    "\\begin{equation}\n",
    "\\text{Max} \\quad Z = \\sum_{k \\in K}  \\sum_{i \\in I^{k}} \\sum_{j \\in J^{k}} r_{k,i,j} \\cdot x_{k,i,j}\n",
    "\\tag{0}\n",
    "\\end{equation}\n",
    "\n",
    "### Constraints\n",
    "\n",
    "- **Product offers**. Allocate offers of a product to customers of each cluster.\n",
    "\n",
    "\\begin{equation}\n",
    "\\sum_{i \\in  I^{k}}  x_{k,i,j} = Y_{k,j}  \\quad \\forall j \\in J^{k}, k \\in K\n",
    "\\tag{1}\n",
    "\\end{equation}\n",
    "\n",
    "- **Offers limit**. At most one product may be offered to a customer in each cluster.\n",
    "\n",
    "\\begin{equation}\n",
    "\\sum_{j \\in J^{k}} x_{k,i,j} \\leq 1 \\quad \\forall i \\in I^{k}, k \\in K\n",
    "\\tag{2}\n",
    "\\end{equation}\n",
    "\n",
    "- **Budget**. Enforce budget constraint.\n",
    "\n",
    "\\begin{equation}\n",
    "\\sum_{k \\in K}  \\sum_{i \\in I^{k}} \\sum_{j \\in J^{k}} c_{k,i,j} \\cdot x_{k,i,j} \\leq B'\n",
    "\\tag{3}\n",
    "\\end{equation}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [],
   "source": [
    "### Operational model enforcing constraint for total budget available \n",
    "\n",
    "# Declare and initialize model\n",
    "mob = gp.Model('OperationalB')\n",
    "\n",
    "### Decision variables\n",
    "\n",
    "xb = mob.addVars(ccp, vtype=GRB.BINARY, name=\"assign\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Product offers constraint\n",
    "\n",
    "productOffersb = {}\n",
    "\n",
    "for k in clusters:\n",
    "    for j in products:\n",
    "            productOffersb[k,j] = mob.addConstr(gp.quicksum(xb[k,i,j] for kk,i,jj in ccp if (kk ==k and jj == j)) == \n",
    "                                              int(y[k,j].x), name='prodOffersb_' + str(k) + ',' + str(j) )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [],
   "source": [
    "# limit on the number of offers to each customer in a cluster.\n",
    "\n",
    "customerOffersb = {}\n",
    "\n",
    "for k,i in ki:\n",
    "    customerOffersb[k,i] = mob.addConstr(gp.quicksum(xb[k,i,j] for kk,ii,j in ccp if (kk == k and ii == i) ) <= 1, \n",
    "                                          name ='custOffersb_' + str(k) + ',' + str(i) )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "outputs": [],
   "source": [
    "# budget constraint\n",
    "\n",
    "# New budget\n",
    "new_budget = budget + z.x\n",
    "\n",
    "totBudget = mob.addConstr(xb.prod(customer_cost) <= new_budget, name='total_budget')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "outputs": [],
   "source": [
    "### Objective function\n",
    "\n",
    "# Maximize total profit\n",
    "\n",
    "mob.setObjective(xb.prod(customer_profit), GRB.MAXIMIZE)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Gurobi Optimizer version 9.1.0 build v9.1.0rc0 (win64)\n",
      "Thread count: 4 physical cores, 8 logical processors, using up to 8 threads\n",
      "Optimize a model with 15 rows, 20 columns and 60 nonzeros\n",
      "Model fingerprint: 0x96da858f\n",
      "Variable types: 0 continuous, 20 integer (20 binary)\n",
      "Coefficient statistics:\n",
      "  Matrix range     [1e+00, 3e+02]\n",
      "  Objective range  [9e+02, 3e+03]\n",
      "  Bounds range     [1e+00, 1e+00]\n",
      "  RHS range        [1e+00, 6e+02]\n",
      "Found heuristic solution: objective 5950.0000000\n",
      "Presolve removed 7 rows and 10 columns\n",
      "Presolve time: 0.00s\n",
      "Presolved: 8 rows, 10 columns, 28 nonzeros\n",
      "Variable types: 0 continuous, 10 integer (10 binary)\n",
      "\n",
      "Root relaxation: objective 6.000000e+03, 2 iterations, 0.00 seconds\n",
      "\n",
      "    Nodes    |    Current Node    |     Objective Bounds      |     Work\n",
      " Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time\n",
      "\n",
      "*    0     0               0    6000.0000000 6000.00000  0.00%     -    0s\n",
      "\n",
      "Explored 0 nodes (2 simplex iterations) in 0.02 seconds\n",
      "Thread count was 8 (of 8 available processors)\n",
      "\n",
      "Solution count 2: 6000 5950 \n",
      "\n",
      "Optimal solution found (tolerance 1.00e-04)\n",
      "Best objective 6.000000000000e+03, best bound 6.000000000000e+03, gap 0.0000%\n"
     ]
    }
   ],
   "source": [
    "# Verify model formulation\n",
    "\n",
    "mob.write('operationalB.lp')\n",
    "\n",
    "# Run optimization engine\n",
    "\n",
    "mob.optimize()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "Optimal assignment of product offers to customers.\n",
      "___________________________________________________\n",
      "Customer c1 in cluster k1 gets an offer of product p2:\n",
      "The expected profit is $1,050.00 at a cost of $105.00\n",
      "Customer c2 in cluster k1 gets an offer of product p1:\n",
      "The expected profit is $1,950.00 at a cost of $195.00\n",
      "Customer c4 in cluster k1 gets an offer of product p1:\n",
      "The expected profit is $2,100.00 at a cost of $210.00\n",
      "Customer c5 in cluster k1 gets an offer of product p2:\n",
      "The expected profit is $900.00 at a cost of $90.00\n",
      "___________________________________________________\n",
      "Number of assignments in cluster k1 is 4\n",
      "___________________________________________________\n",
      "___________________________________________________\n",
      "Number of assignments in cluster k2 is 0\n",
      "___________________________________________________\n",
      "\n",
      "\n",
      "Financial reports.\n",
      "___________________________________________________\n",
      "Optimal total customers profit is $6,000.00.\n",
      "Optimal total customers cost is $600.00 with a budget of $200.00 and an extra amount of $400.00.\n",
      "Optimal ROI is 1000.0% with a minimum ROI of  120.0%.\n"
     ]
    }
   ],
   "source": [
    "### Output Reports\n",
    "\n",
    "# Optimal assignment of product offers to customers\n",
    "\n",
    "total_customer_profitb = 0\n",
    "total_customer_costb = 0\n",
    "\n",
    "kvalueb = None\n",
    "firstb = True\n",
    "num_assignmentsb = 0\n",
    "\n",
    "print(\"\\nOptimal assignment of product offers to customers.\")\n",
    "print(\"___________________________________________________\")\n",
    "for k,i,j in ccp:\n",
    "    if k != kvalueb:\n",
    "        prevkb = kvalueb\n",
    "        kvalueb = k\n",
    "        if not firstb:\n",
    "            print(\"___________________________________________________\")\n",
    "            print(f\"Number of assignments in cluster {prevkb} is {num_assignmentsb}\")\n",
    "            print(\"___________________________________________________\")\n",
    "            num_assignmentsb = 0\n",
    "        if firstb:\n",
    "            firstb = False\n",
    "    if xb[k,i,j].x > 0.5:\n",
    "        #print(x[k,i,j].varName, x[k,i,j].x)\n",
    "        profitb = '${:,.2f}'.format(customer_profit[k,i,j])\n",
    "        costb = '${:,.2f}'.format(customer_cost[k,i,j])\n",
    "        print(f\"Customer {i} in cluster {k} gets an offer of product {j}:\")\n",
    "        print(f\"The expected profit is {profitb} at a cost of {costb}\")\n",
    "        total_customer_profitb += customer_profit[k,i,j]*xb[k,i,j].x\n",
    "        total_customer_costb += customer_cost[k,i,j]*xb[k,i,j].x\n",
    "        num_assignmentsb += 1\n",
    "print(\"___________________________________________________\")\n",
    "print(f\"Number of assignments in cluster {kvalueb} is {num_assignmentsb}\")\n",
    "print(\"___________________________________________________\\n\")\n",
    "        \n",
    "# Financial reports\n",
    "\n",
    "customers_ROIb = round(100*total_customer_profitb/total_customer_costb,2)\n",
    "\n",
    "money_customers_profitb = '${:,.2f}'.format(total_customer_profitb)\n",
    "money_customers_costb = '${:,.2f}'.format(total_customer_costb)\n",
    "\n",
    "print(f\"\\nFinancial reports.\")\n",
    "print(\"___________________________________________________\")\n",
    "print(f\"Optimal total customers profit is {money_customers_profitb}.\")\n",
    "print(f\"Optimal total customers cost is {money_customers_costb} with a budget of {money_budget} and an extra amount of {increased_budget}.\")\n",
    "print(f\"Optimal ROI is {customers_ROIb}% with a minimum ROI of  {min_ROI}%.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Analysis\n",
    "Each customer got, at most, one product offer. Products p1 and p2 are offerred to at least two customers. The cost of assigning  products to customers is $\\$600$, which is equal to the total budget available. The total customers profit is $\\$6,000$. The  ROI is 1,000 %, which is much higher than the minimum ROI required.\n",
    "\n",
    "In this scenario, we enforce the total available budget constraint and we get a different distribution for the assignment. \n",
    "Product p1 is offered to customers c2 and c4, and product p2 is offered to customers c1 and c5. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##  Conclusion\n",
    "\n",
    "In this Jupyter Notebook, we discussed the importance of marketing campaigns for the banking industry. We discussed that machine learning predictive response models can be used to provide the input data of a marketing campaign optimization problem. We showed how the marketing campaign optimization problem can be decomposed into a tactical problem and an operational problem.\n",
    "\n",
    "The tactical problem is formulated as a linear programming problem where we aggregate data generated by the machine learning predictive response models.\n",
    "\n",
    "The solution of the tactical problem determines what products to offer to customers in clusters while maximizing the marketing campaign expected profits and considering the following constraints: \n",
    "\n",
    "* limits on funding available for the campaign.\n",
    "* restrictions on the minimum number of product offers that can be made in a campaign.\n",
    "* campaign return-on-investment hurdle rates that must be met.\n",
    "\n",
    "The operational problem is formulated as a MIP model, where the expected customer  profits and the output of the tactical model can be used as inputs to assign products offers to individual customers in such way that the total customers profit is maximized. We considered two cases for the operational problem. In the first case, the total available budget determined by tactical problem is not enforced. This means that the optimal solution of this problem might have slight violations of the total available budget. In the second case, we enforce a total available budget.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##  References\n",
    "\n",
    "[1] M. D. Cohen. *Exploiting response models—optimizing cross-sell and up-sell opportunities in banking.* Information Systems. Vol. 29. issue 4, June 2004, Pages 327-341"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Copyright © 2020 Gurobi Optimization, LLC"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
